<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, viewport-fit=cover"
    />
    <title>二维码扫描工具</title>
    <link rel="icon" href="./favicon.ico" />
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
        font-family: 'SF Pro Display', 'PingFang SC', 'Helvetica Neue', Arial,
          sans-serif;
      }

      :root {
        --primary-bg: #e6e7ee;
        --secondary-bg: #ffffff;
        --shadow-light: #ffffff;
        --shadow-dark: #d1d9e6;
        --accent-color: #667eea;
        --accent-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        --success-color: #4ecdc4;
        --danger-color: #ff6b6b;
        --warning-color: #feca57;
        --text-primary: #2d3748;
        --text-secondary: #718096;
      }

      body {
        background: var(--primary-bg);
        color: var(--text-primary);
        min-height: 100vh;
        padding: 20px;
        display: flex;
        flex-direction: column;
        align-items: center;
        transition: all 0.3s ease;
        animation: pageLoad 0.8s ease;
      }

      @keyframes pageLoad {
        from {
          opacity: 0;
          transform: translateY(20px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .container {
        width: 100%;
        max-width: 1200px;
        background: var(--primary-bg);
        border-radius: 30px;
        padding: 40px;
        box-shadow: 20px 20px 60px var(--shadow-dark),
          -20px -20px 60px var(--shadow-light);
        margin-top: 20px;
        transition: all 0.3s ease;
        animation: containerFloat 1s ease 0.2s both;
      }

      @keyframes containerFloat {
        from {
          opacity: 0;
          transform: translateY(30px) scale(0.95);
        }
        to {
          opacity: 1;
          transform: translateY(0) scale(1);
        }
      }

      .container:hover {
        box-shadow: 25px 25px 70px var(--shadow-dark),
          -25px -25px 70px var(--shadow-light);
      }

      header {
        text-align: center;
        margin-bottom: 40px;
        padding: 30px;
        background: var(--primary-bg);
        border-radius: 25px;
        box-shadow: 10px 10px 20px var(--shadow-dark),
          -10px -10px 20px var(--shadow-light);
        animation: headerSlide 0.8s ease 0.4s both;
      }

      @keyframes headerSlide {
        from {
          opacity: 0;
          transform: translateY(-30px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      h1 {
        font-size: 3.2rem;
        margin-bottom: 15px;
        background: var(--accent-gradient);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
        font-weight: 700;
        animation: titleGlow 3s ease-in-out infinite alternate;
      }

      @keyframes titleGlow {
        from {
          filter: brightness(1);
        }
        to {
          filter: brightness(1.2);
        }
      }

      .subtitle {
        font-size: 1.3rem;
        color: var(--text-secondary);
        margin-bottom: 30px;
        font-weight: 500;
      }

      .panels {
        display: flex;
        gap: 30px;
        margin-bottom: 40px;
        animation: panelsSlide 0.8s ease 0.6s both;
        flex-wrap: wrap;
      }

      @keyframes panelsSlide {
        from {
          opacity: 0;
          transform: translateX(-20px);
        }
        to {
          opacity: 1;
          transform: translateX(0);
        }
      }

      .panel {
        flex: 1;
        min-width: 320px;
        background: var(--primary-bg);
        border-radius: 25px;
        padding: 30px;
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
        transition: all 0.3s ease;
        position: relative;
        overflow: hidden;
      }

      .panel::before {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.1),
          transparent
        );
        transition: left 0.8s;
      }

      .panel:hover::before {
        left: 100%;
      }
      .panel:hover {
        transform: translateY(-5px);
        box-shadow: 16px 16px 32px var(--shadow-dark),
          -16px -16px 32px var(--shadow-light);
      }

      .panel-title {
        font-size: 1.6rem;
        margin-bottom: 20px;
        color: var(--accent-color);
        display: flex;
        align-items: center;
        font-weight: 600;
      }
      .panel-title::before {
        content: attr(data-icon);
        margin-right: 12px;
        font-size: 1.8rem;
        animation: iconBounce 2s ease-in-out infinite;
      }
      @keyframes iconBounce {
        0%,
        100% {
          transform: scale(1);
        }
        50% {
          transform: scale(1.1);
        }
      }

      .file-upload {
        border: 3px dashed var(--accent-color);
        border-radius: 25px;
        padding: 40px;
        text-align: center;
        transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
        margin-bottom: 25px;
        cursor: pointer;
        background: var(--primary-bg);
        box-shadow: inset 8px 8px 16px var(--shadow-dark),
          inset -8px -8px 16px var(--shadow-light);
        position: relative;
        overflow: hidden;
      }
      .file-upload.dragover {
        border-color: var(--success-color);
        background: rgba(78, 205, 196, 0.05);
        transform: scale(1.02);
        box-shadow: inset 12px 12px 24px var(--shadow-dark),
          inset -12px -12px 24px var(--shadow-light),
          0 20px 50px rgba(78, 205, 196, 0.3);
        animation: dragPulse 1s ease-in-out infinite;
      }
      @keyframes dragPulse {
        0%,
        100% {
          border-color: var(--success-color);
        }
        50% {
          border-color: var(--accent-color);
        }
      }

      .file-upload-content {
        position: relative;
        z-index: 1;
      }
      .file-upload-icon {
        font-size: 3rem;
        margin-bottom: 12px;
        display: block;
      }
      .file-upload-text {
        font-size: 1.2rem;
        font-weight: 600;
        color: var(--text-primary);
        margin-bottom: 8px;
        background: var(--accent-gradient);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
      }
      .file-upload-subtext {
        font-size: 1rem;
        color: var(--text-secondary);
      }
      .file-upload input[type='file'] {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        opacity: 0;
        cursor: pointer;
        z-index: 2;
      }

      .video-preview,
      .image-preview {
        width: 100%;
        background: var(--primary-bg);
        border-radius: 20px;
        overflow: hidden;
        margin-top: 15px;
        box-shadow: inset 10px 10px 20px var(--shadow-dark),
          inset -10px -10px 20px var(--shadow-light);
        transition: all 0.3s ease;
      }
      .video-preview:hover,
      .image-preview:hover {
        box-shadow: inset 12px 12px 24px var(--shadow-dark),
          inset -12px -12px 24px var(--shadow-light);
      }

      video {
        width: 100%;
        max-height: 320px;
        display: block;
        border-radius: 12px;
        transform-origin: center center;
        will-change: transform;
        transition: transform 0.12s ease-out;
      }
      img {
        width: 100%;
        max-height: 320px;
        display: block;
        border-radius: 12px;
        object-fit: contain;
      }

      .options-form {
        display: flex;
        gap: 20px;
        flex-wrap: wrap;
        align-items: flex-end;
        margin-top: 15px;
      }
      .form-group {
        flex: 1;
        min-width: 200px;
      }
      label {
        display: block;
        margin-bottom: 8px;
        color: var(--text-secondary);
        font-weight: 500;
        font-size: 0.95rem;
      }
      input,
      select {
        width: 100%;
        padding: 12px 16px;
        background: var(--primary-bg);
        border: none;
        border-radius: 15px;
        color: var(--text-primary);
        font-size: 1rem;
        font-weight: 500;
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
      }
      input:focus,
      select:focus {
        outline: none;
        box-shadow: inset 8px 8px 16px var(--shadow-dark),
          inset -8px -8px 16px var(--shadow-light),
          0 0 0 3px rgba(102, 126, 234, 0.2);
      }

      button {
        background: var(--primary-bg);
        color: var(--text-primary);
        border: none;
        padding: 12px 24px;
        border-radius: 15px;
        cursor: pointer;
        font-weight: 600;
        transition: all 0.3s ease;
        font-size: 1rem;
        flex-shrink: 0;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
        position: relative;
        overflow: hidden;
      }
      button:hover {
        transform: translateY(-2px);
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
      }
      button:disabled {
        opacity: 0.6;
        cursor: not-allowed;
      }
      .btn-primary {
        background: var(--accent-gradient);
        color: white;
      }
      .btn-success {
        background: linear-gradient(
          135deg,
          var(--success-color) 0%,
          #44a08d 100%
        );
        color: white;
      }
      .btn-danger {
        background: linear-gradient(
          135deg,
          var(--danger-color) 0%,
          #d65757 100%
        );
        color: white;
      }

      .status-message {
        min-height: 28px;
        margin: 14px 0;
        color: var(--accent-color);
        font-weight: 600;
        font-size: 1rem;
      }
      .error-message {
        color: var(--danger-color);
        min-height: 28px;
        font-weight: 600;
        font-size: 1rem;
      }

      .result-panel {
        text-align: center;
        animation: resultPanelSlide 0.8s ease 1.2s both;
      }
      @keyframes resultPanelSlide {
        from {
          opacity: 0;
          transform: translateY(30px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }
      .result-box {
        width: 100%;
        padding: 16px;
        border-radius: 18px;
        background: var(--primary-bg);
        box-shadow: inset 8px 8px 16px var(--shadow-dark),
          inset -8px -8px 16px var(--shadow-light);
        min-height: 70px;
        text-align: left;
        white-space: pre-wrap;
        word-break: break-all;
      }
      .actions {
        display: flex;
        justify-content: center;
        gap: 16px;
        margin-top: 16px;
      }

      .instructions {
        background: var(--primary-bg);
        border-radius: 25px;
        padding: 30px;
        margin-top: 30px;
        box-shadow: 10px 10px 20px var(--shadow-dark),
          -10px -10px 20px var(--shadow-light);
      }
      .instructions h3 {
        color: var(--accent-color);
        margin-bottom: 14px;
        font-size: 1.3rem;
        font-weight: 600;
      }
      .instructions ol {
        padding-left: 22px;
      }
      .instructions li {
        margin-bottom: 10px;
        line-height: 1.7;
        color: var(--text-secondary);
        font-weight: 500;
      }

      footer {
        margin-top: 30px;
        text-align: center;
        color: var(--text-secondary);
        font-size: 0.95rem;
        padding: 25px;
        background: var(--primary-bg);
        border-radius: 20px;
        box-shadow: 6px 6px 12px var(--shadow-dark),
          -6px -6px 12px var(--shadow-light);
      }
      /* Mobile UI 优化 */
      @supports (padding: env(safe-area-inset-top)) {
        body {
          padding-top: calc(20px + env(safe-area-inset-top));
          padding-bottom: calc(20px + env(safe-area-inset-bottom));
          padding-left: calc(20px + env(safe-area-inset-left));
          padding-right: calc(20px + env(safe-area-inset-right));
        }
      }

      @media (max-width: 768px) {
        h1 {
          font-size: 2rem;
        }
        .subtitle {
          font-size: 1rem;
        }
        .container {
          padding: 18px;
          border-radius: 22px;
        }
        .panels {
          flex-direction: column;
          gap: 16px;
        }
        .panel {
          min-width: 100%;
          padding: 18px;
          border-radius: 18px;
        }
        .panel-title {
          font-size: 1.25rem;
        }
        .file-upload {
          padding: 24px;
        }
        .video-preview,
        .image-preview {
          border-radius: 16px;
        }
        video,
        img {
          max-height: 45vh;
        }
        .options-form {
          gap: 12px;
        }
        .actions {
          flex-direction: column;
          gap: 10px;
        }
        button {
          width: 100%;
        }
      }

      @media (max-width: 480px) {
        h1 {
          font-size: 1.6rem;
        }
        .subtitle {
          font-size: 0.95rem;
        }
        .container {
          padding: 14px;
        }
        .panel {
          padding: 14px;
        }
        label {
          font-size: 0.9rem;
        }
        input,
        select {
          padding: 10px 12px;
          font-size: 0.95rem;
        }
        .actions {
          gap: 8px;
        }
      }
      /* Tabs 样式 */
      .tabs {
        display: flex;
        gap: 12px;
        margin-bottom: 20px;
        background: var(--primary-bg);
        border-radius: 18px;
        padding: 10px;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
      }
      .tab {
        flex: 1;
        padding: 12px 16px;
        border-radius: 12px;
        background: var(--primary-bg);
        color: var(--text-primary);
        cursor: pointer;
        font-weight: 600;
        text-align: center;
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
        border: none;
      }
      .tab.active {
        background: var(--accent-gradient);
        color: #fff;
        box-shadow: none;
      }
      .hidden {
        display: none !important;
      }
      /* Zoom 控件样式 */
      .zoom-row {
        display: flex;
        align-items: center;
        flex-wrap: wrap;
        gap: 10px;
      }
      .zoom-btn {
        padding: 10px 14px;
        border-radius: 12px;
        font-weight: 700;
        background: var(--primary-bg);
        color: var(--text-primary);
        border: none;
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
      }
      .zoom-btn:active {
        transform: scale(0.98);
      }
      #zoomRange {
        flex: 1;
        accent-color: #667eea;
      }

      @media (max-width: 768px) {
        .tabs {
          gap: 10px;
          padding: 8px;
        }
        .tab {
          padding: 10px 12px;
        }
      }
    </style>
    <style>
      /* Switch 开关样式（无障碍） */
      .switch {
        display: inline-flex;
        align-items: center;
        gap: 10px;
        cursor: pointer;
        user-select: none;
      }
      .switch input {
        position: absolute;
        opacity: 0;
        width: 0;
        height: 0;
      }
      .switch-slider {
        width: 46px;
        height: 26px;
        background: #cfd8e3;
        border-radius: 13px;
        position: relative;
        box-shadow: inset 2px 2px 6px var(--shadow-dark),
          inset -2px -2px 6px var(--shadow-light);
        transition: background 0.2s ease;
      }
      .switch-slider::after {
        content: '';
        position: absolute;
        width: 20px;
        height: 20px;
        left: 3px;
        top: 3px;
        background: #fff;
        border-radius: 50%;
        box-shadow: 2px 2px 6px var(--shadow-dark),
          -2px -2px 6px var(--shadow-light);
        transition: transform 0.2s ease;
      }
      .switch input:checked + .switch-slider {
        background: #8ea6ff;
      }
      .switch input:checked + .switch-slider::after {
        transform: translateX(20px);
      }
      .switch-label {
        color: var(--text-secondary);
        font-size: 14px;
      }

      /* 裁剪弹框 */
      .modal {
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.35);
        display: none;
        align-items: center;
        justify-content: center;
        padding: 16px;
        z-index: 9999;
      }
      .modal.show {
        display: flex;
      }
      .modal-content {
        background: var(--secondary-bg);
        border-radius: 16px;
        width: min(960px, 96vw);
        max-height: 92vh;
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
        display: flex;
        flex-direction: column;
        overflow: hidden;
      }
      .modal-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 14px 18px;
        font-weight: 600;
        border-bottom: 1px solid #e5e7eb;
      }
      .modal-close {
        background: transparent;
        border: none;
        color: var(--text-primary);
        font-size: 22px;
        line-height: 1;
        padding: 6px 10px;
        border-radius: 10px;
        cursor: pointer;
        width: 50px;
      }
      .modal-body {
        padding: 10px;
        overflow: auto;
      }
      #cropImage {
        max-width: 100%;
        height: auto;
        display: block;
      }
      .modal-actions {
        display: flex;
        gap: 10px;
        justify-content: flex-end;
        padding: 12px 16px;
        border-top: 1px solid #e5e7eb;
      }
      .modal-status {
        flex: 1;
        color: var(--text-secondary);
        align-self: center;
        font-size: 0.95rem;
      }

      /* 摄像头拍照全屏叠层（移动端） */
      .camera-modal {
        position: fixed;
        inset: 0;
        background: #000;
        display: none;
        z-index: 10000;
      }
      .camera-modal.show {
        display: block;
      }
      .camera-header {
        position: absolute;
        top: env(safe-area-inset-top, 12px);
        left: 12px;
        right: 12px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        color: #fff;
        font-weight: 600;
        padding: 8px 4px;
        pointer-events: none;
      }
      .camera-close {
        pointer-events: auto;
        background: rgba(255, 255, 255, 0.1);
        color: #fff;
        border: none;
        border-radius: 12px;
        width: 44px;
        height: 44px;
        font-size: 24px;
        line-height: 1;
      }
      .camera-body {
        position: absolute;
        inset: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        background: #000;
      }
      #cameraLive,
      #cameraStill {
        width: 100vw;
        height: 100vh;
        object-fit: cover;
      }
      .camera-fab {
        position: absolute;
        bottom: calc(env(safe-area-inset-bottom, 16px) + 26px);
        left: 50%;
        transform: translateX(-50%);
        width: 72px;
        height: 72px;
        border-radius: 50%;
        background: #fff;
        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
        border: 6px solid rgba(255, 255, 255, 0.35);
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        font-weight: 700;
        font-size: 13px;
        color: #111;
      }
      .camera-actions {
        position: absolute;
        bottom: calc(env(safe-area-inset-bottom, 16px) + 16px);
        left: 0;
        right: 0;
        display: flex;
        gap: 12px;
        justify-content: center;
        padding: 0 20px;
      }
      .camera-btn {
        width: 60px;
        height: 60px;
        border-radius: 50%;
        background: rgba(255, 255, 255, 0.15);
        color: #fff;
        border: 2px solid rgba(255, 255, 255, 0.5);
        font-size: 24px;
        line-height: 60px;
        text-align: center;
        box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
      }
      .camera-retake {
        border-color: var(--danger-color);
      }
      .camera-confirm {
        border-color: var(--success-color);
      }
      .camera-modal-status {
        position: absolute;
        bottom: calc(env(safe-area-inset-bottom, 16px) + 100px);
        left: 0;
        right: 0;
        text-align: center;
        color: #fff;
        font-size: 0.95rem;
      }
      .camera-side-btn {
        position: absolute;
        bottom: calc(env(safe-area-inset-bottom, 16px) + 46px);
        width: 52px;
        height: 52px;
        border-radius: 50%;
        background: rgba(255, 255, 255, 0.15);
        color: #fff;
        border: 2px solid rgba(255, 255, 255, 0.5);
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 22px;
        box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
      }
      .camera-side-btn.left {
        left: 22px;
      }
      .camera-side-btn.right {
        right: 22px;
      }
      .camera-side-btn.on {
        background: rgba(255, 255, 255, 0.25);
        border-color: var(--warning-color);
      }

      /* 缩放滑块样式（移动端可见） */
      .zoom-row input[type='range'] {
        flex: 1;
        width: 100%;
        min-width: 140px;
        height: 28px;
        margin: 0 8px;
        background: transparent;
        -webkit-appearance: none;
        appearance: none;
        touch-action: pan-y;
      }
      .zoom-row input[type='range']::-webkit-slider-runnable-track {
        height: 6px;
        background: #cfd8e3;
        border-radius: 3px;
      }
      .zoom-row input[type='range']::-webkit-slider-thumb {
        -webkit-appearance: none;
        width: 20px;
        height: 20px;
        margin-top: -7px;
        background: #8ea6ff;
        border-radius: 50%;
        box-shadow: 2px 2px 6px var(--shadow-dark),
          -2px -2px 6px var(--shadow-light);
      }
      .zoom-row input[type='range']::-moz-range-track {
        height: 6px;
        background: #cfd8e3;
        border-radius: 3px;
      }
      .zoom-row input[type='range']::-moz-range-thumb {
        width: 20px;
        height: 20px;
        background: #8ea6ff;
        border: none;
        border-radius: 50%;
      }

      /* 移动端适配增强 */
      @media (max-width: 640px) {
        .container {
          padding: 16px;
        }
        .file-upload {
          padding: 16px;
        }
        .panel-title {
          font-size: 18px;
        }
        .zoom-row {
          gap: 8px;
        }
        .actions {
          flex-wrap: wrap;
        }
        /* 弹框移动端全屏与底部操作区 */
        .modal {
          padding: 0;
        }
        .modal-content {
          width: 100vw;
          height: 100vh;
          border-radius: 0;
        }
        .modal-header {
          font-size: 16px;
          padding: 12px 14px;
        }
        .modal-body {
          flex: 1;
          padding: 8px;
          overflow: auto;
        }
        .modal-actions {
          position: sticky;
          bottom: 0;
          background: var(--secondary-bg);
          border-top: 1px solid #e5e7eb;
          padding: 12px;
          display: grid;
          grid-template-columns: 1fr 1fr;
          gap: 10px;
        }
        .modal-status {
          grid-column: 1 / -1;
          text-align: center;
          color: var(--text-secondary);
          font-size: 0.95rem;
        }
        .modal-actions button {
          width: 100%;
          padding: 14px 18px;
          font-size: 1rem;
        }
      }

      #cameraConfirmBtn,
      #cameraRetakeBtn {
        display: flex;
        align-items: center;
        justify-content: center;
      }
    </style>
    <link rel="stylesheet" href="./videoFormat/cropper.min.css" />
  </head>
  <body>
    <div class="container">
      <header>
        <h1>二维码扫描工具</h1>
        <p class="subtitle">
          支持本地图片识别或摄像头扫码，识别到 URL 可自动跳转
        </p>
      </header>

      <nav class="tabs" role="tablist" aria-label="功能选择">
        <button
          class="tab active"
          id="tabImage"
          role="tab"
          aria-controls="panelImage"
          aria-selected="true"
        >
          图片识别
        </button>
        <button
          class="tab"
          id="tabCamera"
          role="tab"
          aria-controls="panelCamera"
          aria-selected="false"
        >
          摄像头扫码
        </button>
      </nav>

      <div class="panels">
        <!-- 图片识别 -->
        <section
          class="panel"
          id="panelImage"
          role="tabpanel"
          aria-label="图片识别"
          aria-labelledby="tabImage"
        >
          <div class="panel-title" data-icon="🖼️">图片识别</div>
          <div class="file-upload" id="imageDrop">
            <div class="file-upload-content">
              <span class="file-upload-icon">📁</span>
              <div class="file-upload-text">点击或拖放图片到此处</div>
              <div class="file-upload-subtext">
                支持常见格式：PNG、JPG、JPEG、WEBP
              </div>
            </div>
            <input type="file" id="imageInput" accept="image/*" />
          </div>
          <div class="image-preview">
            <img id="imagePreview" alt="图片预览" />
          </div>
          <div class="status-message" id="imageStatus"></div>
          <div class="error-message" id="imageError"></div>
          <div class="actions">
            <button class="btn-secondary" id="pasteImageBtn">
              从剪贴板粘贴
            </button>
            <button class="btn-primary" id="cropStart">进入裁剪</button>
          </div>
        </section>

        <!-- 摄像头扫码 -->
        <section
          class="panel hidden"
          id="panelCamera"
          role="tabpanel"
          aria-label="摄像头扫码"
          aria-labelledby="tabCamera"
        >
          <div class="panel-title" data-icon="📷">摄像头扫码</div>
          <div class="options-form">
            <div class="form-group">
              <label for="cameraSelect">选择摄像头</label>
              <select id="cameraSelect"></select>
            </div>
            <div class="form-group" style="max-width: 260px">
              <label class="switch" for="stopOnFirst">
                <input type="checkbox" id="stopOnFirst" checked />
                <span class="switch-slider" aria-hidden="true"></span>
                <span class="switch-label">识别到后停止</span>
              </label>
            </div>
            <div class="form-group" style="min-width: 260px; flex: 2">
              <label for="zoomRange">缩放（×）</label>
              <div class="zoom-row">
                <button id="zoomMinus" type="button" class="zoom-btn">
                  －
                </button>
                <input
                  type="range"
                  id="zoomRange"
                  min="1"
                  max="3"
                  step="0.1"
                  value="1"
                />
                <button id="zoomPlus" type="button" class="zoom-btn">＋</button>
                <span
                  id="zoomLabel"
                  aria-live="polite"
                  style="
                    min-width: 60px;
                    text-align: right;
                    display: inline-block;
                  "
                  >1.00×</span
                >
              </div>
              <div
                class="zoom-tip"
                style="
                  margin-top: 6px;
                  color: var(--text-secondary);
                  font-size: 0.9rem;
                "
              >
                支持硬件PTZ时将使用摄像头缩放；否则为数字缩放（居中裁剪）。
              </div>
            </div>
          </div>
          <div class="video-preview">
            <video id="video" muted playsinline></video>
          </div>
          <div class="status-message" id="cameraStatus"></div>
          <div class="error-message" id="cameraError"></div>
          <div class="actions">
            <button class="btn-success" id="startBtn">开始扫码</button>
            <button class="btn-danger" id="stopBtn" disabled>停止</button>
          </div>
        </section>
      </div>

      <!-- 结果与操作 -->
      <section class="panel result-panel" aria-label="识别结果">
        <div class="panel-title" data-icon="✅">识别结果</div>
        <div class="options-form">
          <div class="form-group" style="max-width: 260px">
            <label class="switch" for="autoJump">
              <input type="checkbox" id="autoJump" checked />
              <span class="switch-slider" aria-hidden="true"></span>
              <span class="switch-label">识别到 URL 时自动跳转</span>
            </label>
          </div>
          <div class="form-group" style="max-width: 260px">
            <label class="switch" for="openNewTab">
              <input type="checkbox" id="openNewTab" />
              <span class="switch-slider" aria-hidden="true"></span>
              <span class="switch-label">新窗口打开</span>
            </label>
          </div>
        </div>
        <div
          id="resultBox"
          class="result-box"
          aria-live="polite"
          aria-atomic="true"
        ></div>
        <div class="actions">
          <button class="btn-primary" id="copyBtn">复制结果</button>
          <button class="btn-success" id="openBtn">打开链接</button>
          <button id="clearBtn">清空</button>
        </div>
      </section>

      <section class="instructions">
        <h3>使用说明</h3>
        <ol>
          <li>图片识别：点击或拖拽图片到上传区域，自动开始识别。</li>
          <li>摄像头扫码：允许摄像头权限后选择设备，点击「开始扫码」。</li>
          <li>识别到 URL 时可自动跳转；非 URL 内容会显示在结果区域。</li>
          <li>若自动跳转过快，可关闭「识别到后停止」或关闭「自动跳转」。</li>
        </ol>
      </section>

      <footer>© 2025 二维码扫描工具 · 本地识别不上传数据</footer>
    </div>

    <!-- 裁剪弹框 -->
    <div
      id="cropModal"
      class="modal"
      role="dialog"
      aria-modal="true"
      aria-labelledby="cropModalTitle"
    >
      <div class="modal-content">
        <div class="modal-header" id="cropModalTitle">
          <span>裁剪图片并识别二维码</span>
          <button class="modal-close" id="cropClose" aria-label="关闭">
            ×
          </button>
        </div>
        <div class="modal-body">
          <img id="cropImage" alt="裁剪预览" />
        </div>
        <div class="modal-actions">
          <div class="modal-status" id="cropStatus" aria-live="polite"></div>
          <button class="btn-success" id="cropConfirm">识别裁剪区域</button>
          <button class="btn-danger" id="cropCancel">取消</button>
        </div>
      </div>
    </div>

    <!-- 摄像头拍照全屏叠层（移动端） -->
    <div
      id="cameraModal"
      class="camera-modal"
      role="dialog"
      aria-modal="true"
      aria-labelledby="cameraModalTitle"
    >
      <div class="camera-header" id="cameraModalTitle">
        <span>拍照识别</span>
        <button class="camera-close" id="cameraClose" aria-label="关闭">
          ×
        </button>
      </div>
      <div class="camera-body">
        <video id="cameraLive" autoplay muted playsinline class=""></video>
        <img id="cameraStill" alt="拍摄预览" class="hidden" />
        <canvas id="cameraCanvas" class="hidden"></canvas>
      </div>
      <div class="camera-fab" id="cameraCaptureBtn" aria-label="拍照"></div>
      <button class="camera-side-btn left" id="cameraExitBtn" aria-label="退出">
        ✕
      </button>
      <button
        class="camera-side-btn right"
        id="cameraTorchBtn"
        aria-label="手电筒"
      >
        🔦
      </button>
      <div class="camera-actions hidden" id="cameraActions">
        <button
          class="camera-btn camera-retake"
          id="cameraRetakeBtn"
          aria-label="重拍"
        >
          ↺
        </button>
        <button
          class="camera-btn camera-confirm"
          id="cameraConfirmBtn"
          aria-label="确认"
        >
          ✔
        </button>
      </div>
      <div
        class="camera-modal-status"
        id="cameraModalStatus"
        aria-live="polite"
      ></div>
    </div>

    <script src="./qrcode/jsQR.min.js"></script>
    <script src="./videoFormat/cropper.min.js"></script>
    <script>
      // 使用 jsQR 进行二维码识别（不依赖 worker/import）

      const els = {
        imageDrop: document.getElementById('imageDrop'),
        imageInput: document.getElementById('imageInput'),
        imagePreview: document.getElementById('imagePreview'),
        imageStatus: document.getElementById('imageStatus'),
        imageError: document.getElementById('imageError'),

        cameraSelect: document.getElementById('cameraSelect'),
        video: document.getElementById('video'),
        cameraStatus: document.getElementById('cameraStatus'),
        cameraError: document.getElementById('cameraError'),
        startBtn: document.getElementById('startBtn'),
        stopBtn: document.getElementById('stopBtn'),
        stopOnFirst: document.getElementById('stopOnFirst'),
        zoomRange: document.getElementById('zoomRange'),
        zoomPlus: document.getElementById('zoomPlus'),
        zoomMinus: document.getElementById('zoomMinus'),
        zoomLabel: document.getElementById('zoomLabel'),
        videoContainer: document.querySelector('.video-preview'),
        cameraBody: document.querySelector('.camera-body'),

        cropStart: document.getElementById('cropStart'),
        cropConfirm: document.getElementById('cropConfirm'),
        cropCancel: document.getElementById('cropCancel'),
        cropModal: document.getElementById('cropModal'),
        cropImage: document.getElementById('cropImage'),
        cropClose: document.getElementById('cropClose'),
        pasteImageBtn: document.getElementById('pasteImageBtn'),
        cropStatus: document.getElementById('cropStatus'),

        resultBox: document.getElementById('resultBox'),
        copyBtn: document.getElementById('copyBtn'),
        openBtn: document.getElementById('openBtn'),
        clearBtn: document.getElementById('clearBtn'),
        autoJump: document.getElementById('autoJump'),
        openNewTab: document.getElementById('openNewTab'),
        tabImage: document.getElementById('tabImage'),
        tabCamera: document.getElementById('tabCamera'),
        panelImage: document.getElementById('panelImage'),
        panelCamera: document.getElementById('panelCamera'),

        // 移动端拍照叠层元素
        cameraModal: document.getElementById('cameraModal'),
        cameraClose: document.getElementById('cameraClose'),
        cameraLive: document.getElementById('cameraLive'),
        cameraStill: document.getElementById('cameraStill'),
        cameraCanvas: document.getElementById('cameraCanvas'),
        cameraCaptureBtn: document.getElementById('cameraCaptureBtn'),
        cameraActions: document.getElementById('cameraActions'),
        cameraRetakeBtn: document.getElementById('cameraRetakeBtn'),
        cameraConfirmBtn: document.getElementById('cameraConfirmBtn'),
        cameraModalStatus: document.getElementById('cameraModalStatus'),
        cameraTorchBtn: document.getElementById('cameraTorchBtn'),
        cameraExitBtn: document.getElementById('cameraExitBtn'),
      }

      let lastResultText = ''
      let rafId = null
      let stream = null
      let scanning = false
      // 用户是否主动选择了摄像头（用于默认后置优先策略）
      let userSelectedCamera = false
      // 缩放相关：数字缩放与硬件PTZ
      let zoomLevel = 1
      let hardwareZoomSupported = false
      let currentTrack = null
      // 裁剪相关
      let cropper = null
      let cropping = false
      let imageFile = null // 保存最近一次选择的图片，用于DataURL回退
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')

      // 移动端拍照叠层状态
      let modalStream = null
      let photoMode = false
      let torchEnabled = false

      function setStatus(el, text) {
        el.textContent = text || ''
      }
      function setError(el, text) {
        el.textContent = text || ''
      }

      function isProbablyUrl(text) {
        if (!text) return false
        const t = text.trim()
        if (/^(https?:\/\/)/i.test(t)) return true
        if (/^www\./i.test(t)) return true
        return /\.[a-z]{2,}($|\/|\?|#)/i.test(t)
      }
      // 摄像头标签转译：可识别前置/后置，否则返回空串以原样展示
      function translateCameraLabel(label) {
        if (!label) return ''
        const l = label.toLowerCase()
        const frontPatterns = [
          /front/,
          /\buser\b/,
          /facetime/,
          /selfie/,
          /前置/,
          /自拍/,
        ]
        const backPatterns = [
          /back/,
          /\brear\b/,
          /environment/,
          /\bworld\b/,
          /后置/,
          /主摄/,
          /主相机/,
        ]
        if (frontPatterns.some((re) => re.test(l))) return '前置摄像头'
        if (backPatterns.some((re) => re.test(l))) return '后置摄像头'
        return ''
      }
      function normalizeUrl(text) {
        if (!text) return null
        let t = text.trim()
        if (/^www\./i.test(t)) return 'https://' + t
        if (/^(https?:\/\/)/i.test(t)) return t
        if (/^[\w.-]+\.[a-z]{2,}($|\/|\?|#)/i.test(t)) return 'https://' + t
        return null
      }
      function updateResult(text) {
        lastResultText = text || ''
        els.resultBox.textContent = lastResultText || '（暂无识别结果）'
      }

      // 图片识别（增强版 jsQR：多尺度、旋转、对比度/伽马优化）
      async function handleImageFile(file) {
        if (!file) return
        imageFile = file
        setError(els.imageError, '')
        setStatus(els.imageStatus, '正在识别…（增强模式，多策略尝试）')

        const reader = new FileReader()
        reader.onload = () => {
          const dataUrl = reader.result
          els.imagePreview.src = dataUrl
          try {
            const img = new Image()
            img.onload = () => {
              // 基础缩放，避免过大或过小导致采样不佳
              const iw = img.naturalWidth || img.width
              const ih = img.naturalHeight || img.height
              const maxTarget = 1400
              const minTarget = 320
              const maxDim = Math.max(iw, ih)
              let baseScale = 1
              if (maxDim > maxTarget) baseScale = maxTarget / maxDim
              if (maxDim < minTarget)
                baseScale = Math.max(1.5, minTarget / maxDim)

              const bw = Math.round(iw * baseScale)
              const bh = Math.round(ih * baseScale)
              const base = document.createElement('canvas')
              base.width = bw
              base.height = bh
              const bctx = base.getContext('2d')
              bctx.imageSmoothingQuality = 'high'
              bctx.drawImage(img, 0, 0, bw, bh)

              function enhanceAndScan(canvasToUse) {
                const cctx = canvasToUse.getContext('2d')
                const id = cctx.getImageData(
                  0,
                  0,
                  canvasToUse.width,
                  canvasToUse.height
                )
                // 1) 原图直接尝试
                let code = jsQR(
                  id.data,
                  canvasToUse.width,
                  canvasToUse.height,
                  { inversionAttempts: 'attemptBoth' }
                )
                if (code) return code
                // 2) 提升对比度+轻微增亮
                const d = id.data
                const contrastFactor = 1.25
                const cval = Math.round((contrastFactor - 1) * 255)
                const factor = (259 * (cval + 255)) / (255 * (259 - cval))
                const add = 8
                for (let i = 0; i < d.length; i += 4) {
                  d[i] = Math.max(
                    0,
                    Math.min(255, factor * (d[i] - 128) + 128 + add)
                  )
                  d[i + 1] = Math.max(
                    0,
                    Math.min(255, factor * (d[i + 1] - 128) + 128 + add)
                  )
                  d[i + 2] = Math.max(
                    0,
                    Math.min(255, factor * (d[i + 2] - 128) + 128 + add)
                  )
                }
                code = jsQR(d, canvasToUse.width, canvasToUse.height, {
                  inversionAttempts: 'attemptBoth',
                })
                if (code) return code
                // 3) gamma 矫正（提升暗部细节）
                const invGamma = 1 / 0.9
                const lut = new Uint8Array(256)
                for (let i = 0; i < 256; i++) {
                  lut[i] = Math.min(
                    255,
                    Math.round(Math.pow(i / 255, invGamma) * 255)
                  )
                }
                for (let i = 0; i < d.length; i += 4) {
                  d[i] = lut[d[i]]
                  d[i + 1] = lut[d[i + 1]]
                  d[i + 2] = lut[d[i + 2]]
                }
                code = jsQR(d, canvasToUse.width, canvasToUse.height, {
                  inversionAttempts: 'attemptBoth',
                })
                return code || null
              }

              function rotateCanvas(src, deg) {
                const out = document.createElement('canvas')
                const swap = deg % 180 !== 0
                out.width = swap ? src.height : src.width
                out.height = swap ? src.width : src.height
                const octx = out.getContext('2d')
                octx.translate(out.width / 2, out.height / 2)
                octx.rotate((deg * Math.PI) / 180)
                octx.imageSmoothingEnabled = false
                octx.drawImage(src, -src.width / 2, -src.height / 2)
                return out
              }

              function scaleCanvas(src, scale) {
                const out = document.createElement('canvas')
                out.width = Math.max(1, Math.round(src.width * scale))
                out.height = Math.max(1, Math.round(src.height * scale))
                const octx = out.getContext('2d')
                octx.imageSmoothingEnabled = false
                octx.drawImage(src, 0, 0, out.width, out.height)
                return out
              }

              let code = enhanceAndScan(base)
              const scales = baseScale >= 1 ? [1.2, 1.5] : [0.85, 0.7]
              const rotations = [90, 180, 270]

              if (!code) {
                for (const s of scales) {
                  const c1 = scaleCanvas(base, s)
                  code = enhanceAndScan(c1)
                  if (code) break
                }
              }
              if (!code) {
                for (const deg of rotations) {
                  const c2 = rotateCanvas(base, deg)
                  code = enhanceAndScan(c2)
                  if (code) break
                }
              }

              if (code && code.data) {
                updateResult(code.data)
                setStatus(els.imageStatus, '识别完成')
                autoOpenIfUrl(code.data)
              } else {
                setError(els.imageError, '未识别到二维码（已增强处理）')
                setStatus(
                  els.imageStatus,
                  '识别失败，可点击“进入裁剪”后尝试识别选定区域'
                )
              }
            }
            img.onerror = () => {
              setError(els.imageError, '图片加载失败')
              setStatus(els.imageStatus, '')
            }
            img.src = dataUrl
          } catch (err) {
            console.error(err)
            setError(
              els.imageError,
              '识别失败：' + (err?.message || '未知错误')
            )
            setStatus(els.imageStatus, '')
          }
        }
        reader.onerror = () => {
          setError(els.imageError, '读取图片失败')
          setStatus(els.imageStatus, '')
        }
        reader.readAsDataURL(file)
      }

      function autoOpenIfUrl(text) {
        const url = normalizeUrl(text)
        if (!url) return
        const openNew = !!(els.openNewTab && els.openNewTab.checked)
        const ok = confirm(`检测到链接:\n${url}\n是否跳转？`)
        if (ok) {
          window.open(url, openNew ? '_blank' : '_self')
        }
      }

      function bindImageUpload() {
        els.imageInput.addEventListener('change', () => {
          const file = els.imageInput.files?.[0]
          if (file) handleImageFile(file)
          // 选择新图片时重置裁剪状态
          if (cropping) cancelCropping()
        })
        ;['dragenter', 'dragover'].forEach((ev) => {
          els.imageDrop.addEventListener(ev, (e) => {
            e.preventDefault()
            els.imageDrop.classList.add('dragover')
          })
        })
        ;['dragleave', 'drop'].forEach((ev) => {
          els.imageDrop.addEventListener(ev, (e) => {
            e.preventDefault()
            els.imageDrop.classList.remove('dragover')
          })
        })
        els.imageDrop.addEventListener('drop', (e) => {
          const file = e.dataTransfer?.files?.[0]
          if (file) handleImageFile(file)
          if (cropping) cancelCropping()
        })

        // 裁剪按钮事件绑定
        if (els.cropStart) {
          els.cropStart.addEventListener('click', () => {
            if (!els.imagePreview.src) {
              alert('请先上传图片')
              return
            }
            startCropping()
          })
        }
        if (els.cropConfirm) {
          els.cropConfirm.addEventListener('click', confirmCropping)
        }
        if (els.cropCancel) {
          els.cropCancel.addEventListener('click', cancelCropping)
        }
        if (els.cropClose) {
          els.cropClose.addEventListener('click', cancelCropping)
        }
      }

      function bindClipboardPaste() {
        async function handleClipboardRead() {
          try {
            if (navigator.clipboard && navigator.clipboard.read) {
              const items = await navigator.clipboard.read()
              for (const item of items) {
                const type = item.types.find((t) => /^image\//.test(t))
                if (type) {
                  const blob = await item.getType(type)
                  const ext = (type.split('/')[1] || 'png').toLowerCase()
                  const file = new File([blob], `clipboard.${ext}`, { type })
                  showTab('image')
                  await handleImageFile(file)
                  return true
                }
              }
            }
          } catch (err) {
            console.warn('Clipboard read failed:', err)
          }
          return false
        }

        // 原生 paste 事件（可在非安全上下文下工作）
        document.addEventListener('paste', async (e) => {
          const items = e.clipboardData?.items || []
          for (const it of items) {
            if (it.type && it.type.startsWith('image/')) {
              const file = it.getAsFile()
              if (file) {
                showTab('image')
                await handleImageFile(file)
                return
              }
            }
          }
        })

        // Ctrl/Cmd + V 兜底：尝试使用 navigator.clipboard.read 读取图片
        window.addEventListener('keydown', async (e) => {
          if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'v') {
            const ok = await handleClipboardRead()
            if (ok) e.preventDefault()
          }
        })

        // 点击按钮主动读取剪贴板（需安全上下文）
        if (els.pasteImageBtn) {
          els.pasteImageBtn.addEventListener('click', async () => {
            setStatus(els.imageStatus, '正在读取剪贴板...')
            const ok = await handleClipboardRead()
            if (!ok) {
              setError(
                els.imageError,
                '无法从剪贴板读取图片。请使用快捷键粘贴或在浏览器设置中允许剪贴板读取。'
              )
              setStatus(els.imageStatus, '')
            }
          })
        }
      }

      function startCropping() {
        if (!els.imagePreview.src) {
          alert('请先上传图片')
          return
        }
        try {
          // 销毁旧实例
          if (cropper) {
            try {
              cropper.destroy()
            } catch {}
            cropper = null
          }
          // 将预览图复制到弹框中并打开
          els.cropImage.onload = () => {
            try {
              cropper = new Cropper(els.cropImage, {
                viewMode: 1,
                dragMode: 'move',
                autoCrop: true,
                background: true,
                movable: true,
                zoomable: true,
                scalable: false,
                rotatable: false,
                responsive: true,
              })
              if (els.cropConfirm) els.cropConfirm.disabled = false
              if (els.cropCancel) els.cropCancel.disabled = false
              if (els.cropStatus)
                els.cropStatus.textContent = '请调整裁剪框后点击识别'
            } catch (err) {
              console.error(err)
              setError(
                els.imageError,
                '裁剪初始化失败：' + (err?.message || '未知错误')
              )
            }
          }
          els.cropImage.src = els.imagePreview.src
          els.cropModal.classList.add('show')
          cropping = true
          setStatus(
            els.imageStatus,
            '已打开裁剪弹框，调整选择框后点击“识别裁剪区域”'
          )
          setError(els.imageError, '')
        } catch (err) {
          console.error(err)
          setError(
            els.imageError,
            '裁剪初始化失败：' + (err?.message || '未知错误')
          )
        }
      }

      function cancelCropping() {
        if (cropper) {
          try {
            cropper.destroy()
          } catch {}
          cropper = null
        }
        els.cropModal.classList.remove('show')
        cropping = false
        if (els.cropStatus) els.cropStatus.textContent = ''
        setStatus(els.imageStatus, '已取消裁剪')
      }

      async function confirmCropping() {
        if (!cropper) return
        // 禁用按钮并显示内联状态
        if (els.cropConfirm) els.cropConfirm.disabled = true
        if (els.cropCancel) els.cropCancel.disabled = true
        if (els.cropStatus) els.cropStatus.textContent = '正在识别裁剪区域…'
        try {
          const c = cropper.getCroppedCanvas({
            imageSmoothingEnabled: true,
            imageSmoothingQuality: 'high',
          })
          if (!c || !c.width || !c.height) {
            setError(els.imageError, '裁剪区域太小或无效')
            if (els.cropStatus)
              els.cropStatus.textContent = '裁剪区域太小或无效'
            if (els.cropConfirm) els.cropConfirm.disabled = false
            if (els.cropCancel) els.cropCancel.disabled = false
            return
          }
          setStatus(els.imageStatus, '正在识别裁剪区域…')
          setError(els.imageError, '')
          const code = await scanCanvasWithEnhancements(c)
          if (code && code.data) {
            updateResult(code.data)
            setStatus(els.imageStatus, '识别成功（裁剪区域）')
            if (els.cropStatus) els.cropStatus.textContent = '识别成功'
            autoOpenIfUrl(code.data)
            els.cropModal.classList.remove('show')
            cropping = false
          } else {
            setError(els.imageError, '裁剪区域未识别到二维码')
            setStatus(els.imageStatus, '可继续调整裁剪框后再次识别')
            if (els.cropStatus)
              els.cropStatus.textContent = '未识别到二维码，请调整后重试'
            if (els.cropConfirm) els.cropConfirm.disabled = false
            if (els.cropCancel) els.cropCancel.disabled = false
          }
        } catch (err) {
          console.error(err)
          setError(
            els.imageError,
            '裁剪识别失败：' + (err?.message || '未知错误')
          )
          if (els.cropStatus) els.cropStatus.textContent = '识别失败，请重试'
          if (els.cropConfirm) els.cropConfirm.disabled = false
          if (els.cropCancel) els.cropCancel.disabled = false
        }
      }

      async function scanCanvasWithEnhancements(base) {
        function enhanceAndScan(canvasToUse) {
          const cctx = canvasToUse.getContext('2d')
          const id = cctx.getImageData(
            0,
            0,
            canvasToUse.width,
            canvasToUse.height
          )
          let code = jsQR(id.data, canvasToUse.width, canvasToUse.height, {
            inversionAttempts: 'attemptBoth',
          })
          if (code) return code
          const d = id.data
          const contrastFactor = 1.25
          const cval = Math.round((contrastFactor - 1) * 255)
          const factor = (259 * (cval + 255)) / (255 * (259 - cval))
          const add = 8
          for (let i = 0; i < d.length; i += 4) {
            d[i] = Math.max(0, Math.min(255, factor * (d[i] - 128) + 128 + add))
            d[i + 1] = Math.max(
              0,
              Math.min(255, factor * (d[i + 1] - 128) + 128 + add)
            )
            d[i + 2] = Math.max(
              0,
              Math.min(255, factor * (d[i + 2] - 128) + 128 + add)
            )
          }
          code = jsQR(d, canvasToUse.width, canvasToUse.height, {
            inversionAttempts: 'attemptBoth',
          })
          if (code) return code
          const invGamma = 1 / 0.9
          const lut = new Uint8Array(256)
          for (let i = 0; i < 256; i++) {
            lut[i] = Math.min(
              255,
              Math.round(Math.pow(i / 255, invGamma) * 255)
            )
          }
          for (let i = 0; i < d.length; i += 4) {
            d[i] = lut[d[i]]
            d[i + 1] = lut[d[i + 1]]
            d[i + 2] = lut[d[i + 2]]
          }
          code = jsQR(d, canvasToUse.width, canvasToUse.height, {
            inversionAttempts: 'attemptBoth',
          })
          return code || null
        }
        function rotateCanvas(src, deg) {
          const out = document.createElement('canvas')
          const swap = deg % 180 !== 0
          out.width = swap ? src.height : src.width
          out.height = swap ? src.width : src.height
          const octx = out.getContext('2d')
          octx.translate(out.width / 2, out.height / 2)
          octx.rotate((deg * Math.PI) / 180)
          octx.imageSmoothingEnabled = false
          octx.drawImage(src, -src.width / 2, -src.height / 2)
          return out
        }
        function scaleCanvas(src, scale) {
          const out = document.createElement('canvas')
          out.width = Math.max(1, Math.round(src.width * scale))
          out.height = Math.max(1, Math.round(src.height * scale))
          const octx = out.getContext('2d')
          octx.imageSmoothingEnabled = false
          octx.drawImage(src, 0, 0, out.width, out.height)
          return out
        }
        let code = enhanceAndScan(base)
        if (!code) {
          const scales = [1.2, 1.5, 0.85, 0.7]
          for (const s of scales) {
            const c1 = scaleCanvas(base, s)
            code = enhanceAndScan(c1)
            if (code) break
          }
        }
        if (!code) {
          for (const deg of [90, 180, 270]) {
            const c2 = rotateCanvas(base, deg)
            code = enhanceAndScan(c2)
            if (code) break
          }
        }
        return code || null
      }

      async function listCameras() {
        try {
          const devices = await navigator.mediaDevices.enumerateDevices()
          const vids = devices.filter((d) => d.kind === 'videoinput')
          els.cameraSelect.innerHTML = ''
          vids.forEach((d, idx) => {
            const opt = document.createElement('option')
            opt.value = d.deviceId
            const translated = translateCameraLabel(d.label)
            opt.textContent = translated || d.label || '摄像头 ' + (idx + 1)
            els.cameraSelect.appendChild(opt)
          })
          setStatus(
            els.cameraStatus,
            vids.length ? '摄像头已准备' : '未检测到摄像头'
          )
          if (!vids.length) setError(els.cameraError, '未检测到可用摄像头设备')
        } catch (err) {
          console.error(err)
          setError(
            els.cameraError,
            '枚举摄像头失败：' + (err?.message || '未知错误')
          )
        }
      }

      async function startScan() {
        try {
          // 若已有流，先停掉
          await stopScan()
          // 若用户未主动选择设备，则忽略下拉框默认值，以后置优先
          const selectedId = userSelectedCamera ? els.cameraSelect.value : ''
          const constraints = {
            video: selectedId
              ? {
                  deviceId: { exact: selectedId },
                  facingMode: 'environment',
                  width: { ideal: 1280 },
                  height: { ideal: 720 },
                }
              : {
                  facingMode: 'environment',
                  width: { ideal: 1280 },
                  height: { ideal: 720 },
                },
          }
          stream = await navigator.mediaDevices.getUserMedia(constraints)
          els.video.srcObject = stream
          await els.video.play()
          els.startBtn.disabled = true
          els.stopBtn.disabled = false
          scanning = true
          setError(els.cameraError, '')
          setStatus(els.cameraStatus, '正在扫码… 将二维码对准摄像头')

          // 缩放能力配置（优先硬件PTZ，退化为数字缩放）
          currentTrack = stream.getVideoTracks()[0] || null
          configureZoomForCurrentTrack()

          scanLoop()
        } catch (err) {
          console.error(err)
          setError(
            els.cameraError,
            '开始扫码失败：' + (err?.message || '未知错误')
          )
        }
      }

      function scanLoop() {
        if (!scanning) return
        const src = photoMode ? els.cameraLive : els.video
        const w = src.videoWidth | 0
        const h = src.videoHeight | 0
        if (w > 0 && h > 0) {
          canvas.width = w
          canvas.height = h
          if (!hardwareZoomSupported && zoomLevel > 1) {
            const cropW = Math.floor(w / zoomLevel)
            const cropH = Math.floor(h / zoomLevel)
            const sx = Math.floor((w - cropW) / 2)
            const sy = Math.floor((h - cropH) / 2)
            ctx.drawImage(src, sx, sy, cropW, cropH, 0, 0, w, h)
          } else {
            ctx.drawImage(src, 0, 0, w, h)
          }
          const imageData = ctx.getImageData(0, 0, w, h)
          const code = jsQR(imageData.data, w, h, {
            inversionAttempts: 'attemptBoth',
          })
          if (code && code.data) {
            updateResult(code.data)
            setStatus(els.cameraStatus, '识别到二维码')
            const url = normalizeUrl(code.data)
            if (url) {
              autoOpenIfUrl(code.data)
              if (els.stopOnFirst && els.stopOnFirst.checked) {
                stopScan()
                return
              }
            } else {
              alert('检测到内容')
              stopScan()
              document.querySelector('.result-panel')?.scrollIntoView({
                behavior: 'smooth',
              })
              return
            }
          }
        }
        rafId = requestAnimationFrame(scanLoop)
      }

      function configureZoomForCurrentTrack() {
        // 重置状态
        hardwareZoomSupported = false
        zoomLevel = 1
        if (photoMode) {
          if (els.cameraLive) els.cameraLive.style.transform = ''
        } else {
          if (els.video) els.video.style.transform = ''
        }
        if (!els.zoomRange) return

        // 默认数字缩放范围
        let min = 1,
          max = 3,
          step = 0.1

        try {
          if (
            currentTrack &&
            typeof currentTrack.getCapabilities === 'function'
          ) {
            const caps = currentTrack.getCapabilities()
            if (caps && 'zoom' in caps) {
              hardwareZoomSupported = true
              min = caps.zoom?.min ?? min
              max = caps.zoom?.max ?? max
              step = caps.zoom?.step ?? step
            }
          }
        } catch {}

        els.zoomRange.min = String(min)
        els.zoomRange.max = String(max)
        els.zoomRange.step = String(step)
        const initial =
          hardwareZoomSupported &&
          typeof currentTrack?.getSettings === 'function'
            ? currentTrack.getSettings().zoom ?? min
            : 1
        els.zoomRange.value = String(initial)
        updateZoomLabel(parseFloat(els.zoomRange.value))
        applyZoom(parseFloat(els.zoomRange.value))
      }

      function updateZoomLabel(v) {
        if (els.zoomLabel) els.zoomLabel.textContent = `${v.toFixed(2)}×`
      }

      function applyZoom(v) {
        if (hardwareZoomSupported && currentTrack) {
          currentTrack
            .applyConstraints({ advanced: [{ zoom: v }] })
            .catch(() => {})
          if (photoMode) {
            if (els.cameraLive) els.cameraLive.style.transform = ''
          } else {
            if (els.video) els.video.style.transform = ''
          }
        } else {
          zoomLevel = Math.max(1, v)
          const target = photoMode ? els.cameraLive : els.video
          if (target) {
            target.style.transformOrigin = 'center center'
            target.style.transform = `scale(${zoomLevel})`
          }
        }
      }

      function bindPinchZoom() {
        const container = els.videoContainer
        if (!container) return
        let startDist = 0
        let startZoom = 1
        function dist(touches) {
          const dx = touches[0].clientX - touches[1].clientX
          const dy = touches[0].clientY - touches[1].clientY
          return Math.hypot(dx, dy)
        }
        container.addEventListener(
          'touchstart',
          (e) => {
            if (e.touches.length === 2) {
              startDist = dist(e.touches)
              startZoom = parseFloat(els.zoomRange.value) || 1
            }
          },
          { passive: true }
        )
        container.addEventListener(
          'touchmove',
          (e) => {
            if (e.touches.length === 2 && startDist) {
              e.preventDefault()
              const ratio = dist(e.touches) / startDist
              const min = parseFloat(els.zoomRange.min) || 1
              const max = parseFloat(els.zoomRange.max) || 3
              let v = Math.min(max, Math.max(min, startZoom * ratio))
              els.zoomRange.value = String(v)
              updateZoomLabel(v)
              applyZoom(v)
            }
          },
          { passive: false }
        )
        container.addEventListener('touchend', () => {
          startDist = 0
        })
      }

      // 拍照叠层双指缩放
      function bindModalPinchZoom() {
        const container = els.cameraBody
        if (!container) return
        let startDist = 0
        let startZoom = 1
        function dist(touches) {
          const dx = touches[0].clientX - touches[1].clientX
          const dy = touches[0].clientY - touches[1].clientY
          return Math.hypot(dx, dy)
        }
        container.addEventListener(
          'touchstart',
          (e) => {
            if (e.touches.length === 2) {
              startDist = dist(e.touches)
              startZoom = parseFloat(els.zoomRange.value) || 1
            }
          },
          { passive: true }
        )
        container.addEventListener(
          'touchmove',
          (e) => {
            if (e.touches.length === 2 && startDist) {
              e.preventDefault()
              const ratio = dist(e.touches) / startDist
              const min = parseFloat(els.zoomRange.min) || 1
              const max = parseFloat(els.zoomRange.max) || 3
              let v = Math.min(max, Math.max(min, startZoom * ratio))
              els.zoomRange.value = String(v)
              updateZoomLabel(v)
              applyZoom(v)
            }
          },
          { passive: false }
        )
        container.addEventListener('touchend', () => {
          startDist = 0
        })
      }

      async function stopScan() {
        scanning = false
        if (rafId) {
          cancelAnimationFrame(rafId)
          rafId = null
        }
        try {
          const s = els.video.srcObject
          if (s) {
            s.getTracks().forEach((t) => t.stop())
            els.video.srcObject = null
          }
        } catch {}
        // 重置缩放状态
        hardwareZoomSupported = false
        zoomLevel = 1
        currentTrack = null
        if (els.video) els.video.style.transform = ''
        els.startBtn.disabled = false
        els.stopBtn.disabled = true
        setStatus(els.cameraStatus, '已停止')
      }

      function isMobileViewport() {
        return window.matchMedia('(max-width: 640px)').matches
      }

      async function startMobileCameraUI() {
        try {
          // 确保普通扫码已停
          await stopScan()
          // 若用户未主动选择设备，则忽略下拉框默认值，以后置优先
          const selectedId = userSelectedCamera ? els.cameraSelect.value : ''
          const constraints = {
            video: selectedId
              ? {
                  deviceId: { exact: selectedId },
                  facingMode: 'environment',
                  width: { ideal: 1280 },
                  height: { ideal: 720 },
                }
              : {
                  facingMode: 'environment',
                  width: { ideal: 1280 },
                  height: { ideal: 720 },
                },
          }
          modalStream = await navigator.mediaDevices.getUserMedia(constraints)
          els.cameraLive.srcObject = modalStream
          await els.cameraLive.play()
          photoMode = true
          els.cameraModal.classList.add('show')
          els.cameraStill.classList.add('hidden')
          els.cameraLive.classList.remove('hidden')
          els.cameraCaptureBtn.classList.remove('hidden')
          els.cameraActions.classList.add('hidden')
          if (els.cameraExitBtn) els.cameraExitBtn.classList.remove('hidden')
          if (els.cameraTorchBtn) els.cameraTorchBtn.classList.remove('hidden')
          if (els.cameraModalStatus)
            els.cameraModalStatus.textContent = '自动扫描中… 将二维码对准摄像头'
          if (els.cameraCaptureBtn)
            els.cameraCaptureBtn.textContent = '自动扫描中'
          // 启动自动扫码（移动端叠层）
          scanning = true
          setError(els.cameraError, '')
          setStatus(els.cameraStatus, '正在扫码… 将二维码对准摄像头')
          currentTrack = modalStream.getVideoTracks()[0] || null
          configureZoomForCurrentTrack()
          scanLoop()
        } catch (err) {
          console.error(err)
          setError(
            els.cameraError,
            '启动拍照模式失败：' + (err?.message || '未知错误')
          )
        }
      }

      function stopModalStream() {
        try {
          const s = els.cameraLive.srcObject
          if (s) s.getTracks().forEach((t) => t.stop())
        } catch {}
        els.cameraLive.srcObject = null
        modalStream = null
      }

      function closeCameraModal() {
        stopModalStream()
        // 关闭手电筒（若开启）
        try {
          setTorch(false)
        } catch {}
        torchEnabled = false
        photoMode = false
        // 停止移动端自动扫码
        scanning = false
        if (rafId) {
          cancelAnimationFrame(rafId)
          rafId = null
        }
        els.cameraModal.classList.remove('show')
        if (els.cameraModalStatus) els.cameraModalStatus.textContent = ''
        // 重置按钮状态
        if (els.cameraConfirmBtn) els.cameraConfirmBtn.disabled = false
        if (els.cameraRetakeBtn) els.cameraRetakeBtn.disabled = false
        if (els.cameraTorchBtn) els.cameraTorchBtn.classList.remove('on')
        if (els.cameraCaptureBtn) els.cameraCaptureBtn.textContent = ''
      }

      function hasTorchCapability() {
        try {
          const caps = currentTrack?.getCapabilities?.()
          return !!(caps && 'torch' in caps)
        } catch {
          return false
        }
      }

      function setTorch(on) {
        if (!currentTrack) return
        try {
          currentTrack
            .applyConstraints({ advanced: [{ torch: !!on }] })
            .catch(() => {})
        } catch {}
      }

      function toggleTorch() {
        if (!hasTorchCapability()) {
          if (els.cameraModalStatus)
            els.cameraModalStatus.textContent = '设备不支持手电筒'
          return
        }
        torchEnabled = !torchEnabled
        setTorch(torchEnabled)
        if (els.cameraTorchBtn)
          els.cameraTorchBtn.classList.toggle('on', torchEnabled)
        if (els.cameraModalStatus)
          els.cameraModalStatus.textContent = torchEnabled
            ? '手电筒已开启'
            : '手电筒已关闭'
      }

      function captureMobileFrame() {
        try {
          const w = els.cameraLive.videoWidth | 0
          const h = els.cameraLive.videoHeight | 0
          if (!w || !h) {
            if (els.cameraModalStatus)
              els.cameraModalStatus.textContent = '摄像头未就绪，请稍后重试'
            return
          }
          els.cameraCanvas.width = w
          els.cameraCanvas.height = h
          const cctx = els.cameraCanvas.getContext('2d')
          if (!hardwareZoomSupported && zoomLevel > 1) {
            const cropW = Math.floor(w / zoomLevel)
            const cropH = Math.floor(h / zoomLevel)
            const sx = Math.floor((w - cropW) / 2)
            const sy = Math.floor((h - cropH) / 2)
            cctx.drawImage(els.cameraLive, sx, sy, cropW, cropH, 0, 0, w, h)
          } else {
            cctx.drawImage(els.cameraLive, 0, 0, w, h)
          }
          const url = els.cameraCanvas.toDataURL('image/png')
          els.cameraStill.src = url
          // 切换为预览+操作按钮
          els.cameraLive.classList.add('hidden')
          els.cameraStill.classList.remove('hidden')
          els.cameraCaptureBtn.classList.add('hidden')
          els.cameraActions.classList.remove('hidden')
          if (els.cameraExitBtn) els.cameraExitBtn.classList.add('hidden')
          if (els.cameraTorchBtn) els.cameraTorchBtn.classList.add('hidden')
          if (els.cameraModalStatus)
            els.cameraModalStatus.textContent = '已拍照，可确认识别或取消重拍'
        } catch (err) {
          console.error(err)
          if (els.cameraModalStatus)
            els.cameraModalStatus.textContent = '拍照失败，请重试'
        }
      }

      function retakeMobileFrame() {
        // 回到实时画面
        els.cameraStill.classList.add('hidden')
        els.cameraLive.classList.remove('hidden')
        els.cameraCaptureBtn.classList.remove('hidden')
        els.cameraActions.classList.add('hidden')
        if (els.cameraExitBtn) els.cameraExitBtn.classList.remove('hidden')
        if (els.cameraTorchBtn) els.cameraTorchBtn.classList.remove('hidden')
        if (els.cameraModalStatus)
          els.cameraModalStatus.textContent = '对准二维码，点击拍照'
      }

      async function confirmMobileFrame() {
        try {
          if (els.cameraConfirmBtn) els.cameraConfirmBtn.disabled = true
          if (els.cameraRetakeBtn) els.cameraRetakeBtn.disabled = true
          if (els.cameraModalStatus)
            els.cameraModalStatus.textContent = '正在识别…'
          const code = await scanCanvasWithEnhancements(els.cameraCanvas)
          if (code && code.data) {
            updateResult(code.data)
            setStatus(els.cameraStatus, '识别成功（拍照）')
            const url = normalizeUrl(code.data)
            if (url) {
              autoOpenIfUrl(code.data)
            } else {
              alert('检测到内容')
            }
            closeCameraModal()
            document.querySelector('.result-panel')?.scrollIntoView({
              behavior: 'smooth',
            })
          } else {
            setError(els.cameraError, '未识别到二维码')
            if (els.cameraModalStatus)
              els.cameraModalStatus.textContent = '未识别到二维码，可重拍'
            if (els.cameraConfirmBtn) els.cameraConfirmBtn.disabled = false
            if (els.cameraRetakeBtn) els.cameraRetakeBtn.disabled = false
          }
        } catch (err) {
          console.error(err)
          setError(
            els.cameraError,
            '拍照识别失败：' + (err?.message || '未知错误')
          )
          if (els.cameraModalStatus)
            els.cameraModalStatus.textContent = '识别失败，请重试'
          if (els.cameraConfirmBtn) els.cameraConfirmBtn.disabled = false
          if (els.cameraRetakeBtn) els.cameraRetakeBtn.disabled = false
        }
      }

      function bindCameraControls() {
        els.startBtn.addEventListener('click', () => {
          if (isMobileViewport()) {
            startMobileCameraUI()
          } else {
            startScan()
          }
        })
        els.stopBtn.addEventListener('click', stopScan)

        // 设备切换时重启扫码
        els.cameraSelect.addEventListener('change', async () => {
          // 标记用户已主动选择摄像头
          userSelectedCamera = true
          if (!scanning) return
          await startScan()
        })

        // 缩放控件事件
        if (els.zoomRange) {
          els.zoomRange.addEventListener('input', (e) => {
            const v = parseFloat(e.target.value)
            updateZoomLabel(v)
            applyZoom(v)
          })
        }
        if (els.zoomPlus) {
          els.zoomPlus.addEventListener('click', () => {
            const step = parseFloat(els.zoomRange.step) || 0.1
            const max = parseFloat(els.zoomRange.max) || 3
            let v = (parseFloat(els.zoomRange.value) || 1) + step
            v = Math.min(max, v)
            els.zoomRange.value = String(v)
            updateZoomLabel(v)
            applyZoom(v)
          })
        }
        if (els.zoomMinus) {
          els.zoomMinus.addEventListener('click', () => {
            const step = parseFloat(els.zoomRange.step) || 0.1
            const min = parseFloat(els.zoomRange.min) || 1
            let v = (parseFloat(els.zoomRange.value) || 1) - step
            v = Math.max(min, v)
            els.zoomRange.value = String(v)
            updateZoomLabel(v)
            applyZoom(v)
          })
        }
      }

      function bindCameraModalUI() {
        if (els.cameraCaptureBtn)
          els.cameraCaptureBtn.addEventListener('click', captureMobileFrame)
        if (els.cameraRetakeBtn)
          els.cameraRetakeBtn.addEventListener('click', retakeMobileFrame)
        if (els.cameraConfirmBtn)
          els.cameraConfirmBtn.addEventListener('click', confirmMobileFrame)
        if (els.cameraClose)
          els.cameraClose.addEventListener('click', closeCameraModal)
        if (els.cameraExitBtn)
          els.cameraExitBtn.addEventListener('click', closeCameraModal)
        if (els.cameraTorchBtn)
          els.cameraTorchBtn.addEventListener('click', toggleTorch)
      }

      function bindResultActions() {
        els.copyBtn.addEventListener('click', async () => {
          const text = lastResultText || ''
          if (!text) return
          try {
            await navigator.clipboard.writeText(text)
            alert('复制成功')
          } catch {
            const ta = document.createElement('textarea')
            ta.value = text
            document.body.appendChild(ta)
            ta.select()
            try {
              document.execCommand('copy')
              alert('复制成功')
            } catch {}
            document.body.removeChild(ta)
          }
        })
        els.openBtn.addEventListener('click', () => {
          const url = normalizeUrl(lastResultText)
          if (!url) {
            alert('识别结果不是 URL')
            return
          }
          window.open(url, '_blank')
        })
        els.clearBtn.addEventListener('click', () => {
          updateResult('')
          els.imagePreview.removeAttribute('src')
          setStatus(els.imageStatus, '')
          setError(els.imageError, '')
          setStatus(els.cameraStatus, '')
          setError(els.cameraError, '')
          if (cropping) cancelCropping()
        })
      }

      function showTab(name) {
        const isImage = name === 'image'
        els.tabImage.classList.toggle('active', isImage)
        els.tabCamera.classList.toggle('active', !isImage)
        els.tabImage.setAttribute('aria-selected', isImage ? 'true' : 'false')
        els.tabCamera.setAttribute('aria-selected', !isImage ? 'true' : 'false')
        els.panelImage.classList.toggle('hidden', !isImage)
        els.panelCamera.classList.toggle('hidden', isImage)
        if (isImage) {
          stopScan()
          setStatus(els.cameraStatus, '')
          setError(els.cameraError, '')
        } else {
          setStatus(els.cameraStatus, '请选择设备后点击“开始扫码”')
        }
      }

      function bindTabEvents() {
        els.tabImage.addEventListener('click', () => showTab('image'))
        els.tabCamera.addEventListener('click', () => showTab('camera'))
      }

      ;(async function init() {
        bindImageUpload()
        bindClipboardPaste()
        bindCameraControls()
        bindCameraModalUI()
        bindModalPinchZoom()
        bindPinchZoom()
        bindResultActions()
        bindTabEvents()
        updateResult('')
        await listCameras()
        showTab('image')
      })()
    </script>
  </body>
</html>
